﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.XPath;

namespace TwinHead.Frameworks.Xml
{
    /// <summary>
    /// XSD/XMM문서를 분석/변환하여 태그구조정보를 생성하는 클래스입니다
    /// </summary>
    public class XmlTranslator : IDisposable
    {
        /// <summary>
        /// 분석/변환 대상유형을 식별합니다.
        /// </summary>
        public enum XmlTranslateType
        {
            /// <summary>
            /// 스키마 입니다.
            /// </summary>
            XSD = 0,
            /// <summary>
            /// 일반 XML문서 입니다.
            /// </summary>
            XML
        }

        private Hashtable nodeHash = new Hashtable();
        private List<IXmlTag> nodeCollection = new List<IXmlTag>();
        private XmlTranslateType translateType = XmlTranslateType.XSD;
        private XPathNavigator navigator = null;
        private XmlNamespaceManager nsmgr = null;

        /// <summary>
        /// 주어진 XPATH에 해당하는 태그를 반환합니다.
        /// </summary>
        /// <param name="xpath"></param>
        /// <returns></returns>
        public IList<IXmlTag> GetXmlTag( string xpath )
        {
            List<IXmlTag> tags = new List<IXmlTag>();
            if (nodeHash.ContainsKey( xpath ))
                tags = nodeHash[xpath] as List<IXmlTag>;

            return tags;
        }

        private void RegistToNodeHash( IXmlTag tag )
        {
            // AppendXmlTag / InsertXmlTag에서만 호출해야 한다. 꼭!!
            string xpath = tag.GetXPath();
            if (!string.IsNullOrEmpty( xpath ))
            {
                if (!nodeHash.ContainsKey( xpath ))
                {
                    List<IXmlTag> buff = new List<IXmlTag>();
                    buff.Add( tag );
                    nodeHash.Add( xpath, buff );
                }
                else
                {
                    List<IXmlTag> buff = nodeHash[xpath] as List<IXmlTag>;
                    buff.Add( tag );
                }
            }

            if (tag.ChildNodesCount > 0)
            {
                foreach (IXmlTag child in tag.ChildNodeCollection)
                    RegistToNodeHash( child );
            }
        }
        private void UnRegistFromNodeHash( IXmlTag tag )
        {
            //RemoveXmlTag에서만 호출해야 한다 꼭!!

            if (tag.ChildNodesCount> 0)
            {
                foreach (IXmlTag child in tag.ChildNodeCollection)
                    UnRegistFromNodeHash( child );
            }

            string xpath = tag.GetXPath();
            if (!string.IsNullOrEmpty( xpath ))
            {
                if (nodeHash.ContainsKey( xpath ))
                {
                    List<IXmlTag> buff = nodeHash[xpath] as List<IXmlTag>;
                    buff.Remove( tag );
                }
            }
        }

        /// <summary>
        /// 부모 태그의 하위요소 목록의 끝에 지정한 하위요소를 추가합니다.
        /// </summary>
        /// <param name="parent"></param>
        /// <param name="child"></param>
        public void AppendXmlTag( IXmlTag parent, IXmlTag child )
        {
            child.ParentNode = parent;
            parent.ChildNodeCollection.Add( child );
            parent.Navigator.AppendChild( child.Navigator );
            string xpath = child.GetXPath();

            RegistToNodeHash( child );
        }
        /// <summary>
        /// 부모 태그의 하위요소 목록의 지정한 인덱스 다음 위치에 지정한 하위요소를 추가합니다.
        /// </summary>
        /// <param name="parent"></param>
        /// <param name="index"></param>
        /// <param name="child"></param>
        public void InsertXmlTag( IXmlTag parent, int index, IXmlTag child )
        {
            child.ParentNode = parent;
            string xpath = child.GetXPath();

            parent.ChildNodeCollection.Insert( index, child );
            parent.Navigator.AppendChild( child.Navigator );
            RegistToNodeHash( child );
        }
        /// <summary>
        /// 태그목록에서 지정한 태그와 그 하위 요소들의 정보를 모두 제거합니다.
        /// </summary>
        /// <param name="tag"></param>
        public void RemoveXmlTag( IXmlTag tag )
        {
            UnRegistFromNodeHash( tag );
            try
            {
                tag.Navigator.DeleteSelf();
            }
            catch
            {
            }

            if (tag.ParentNode != null)
                tag.ParentNode.ChildNodeCollection.Remove( tag );
        }




        
        /// <summary>
        /// 현재 처리하고 있는 문서의 변환대상유형을 설정하거나 가져옵니다.
        /// </summary>
        public XmlTranslateType TranslateType
        {
            get { return translateType; }
            set { translateType = value; }
        }
        
        /// <summary>
        /// 분석/변환결과로 생성되는 문서의 태그구조가 저장된 컬렉션을 가져옵니다.
        /// </summary>
        public IList<IXmlTag> NodeCollection
        {
            get { return nodeCollection; }
        }
        
        /// <summary>
        /// 분석/변환에 사용된 XML 네비게이터 커서 객체를 설정하거나 가져옵니다.
        /// </summary>
        public XPathNavigator Navigator
        {
            get { return navigator; }
            set { navigator = value; }
        }
        
        /// <summary>
        /// 분석/변환한 문서에서 추출한 네임스페이스 매니저를 가져옵니다.
        /// </summary>
        public XmlNamespaceManager CurrentNamespaceManager
        {
            get { return nsmgr; }
        }

        /// <summary>
        /// 분석/변환에 사용된 XML 네비게이터를 재사용하여 변환을 다시 수행합니다.
        /// </summary>
        public void Rebuild()
        {
            nodeHash.Clear();
            XmlTag root = new XmlTag( "root", "" );
            if (TranslateType == XmlTranslateType.XSD)
                InitializeXSD( navigator.SelectChildren( XPathNodeType.Element ), root );
            else
                InitializeXML( navigator.SelectChildren( XPathNodeType.Element ), root );
            nodeCollection.Clear();
            nodeCollection.AddRange( root.ChildNodes.ToArray() );
        }
        /// <summary>
        /// 주어진 XML문서를 기준으로, XML유형으로 변환을 수행합니다.
        /// </summary>
        /// <param name="document">변환/분석 대상이 되는 XML문서 객체</param>
        public void InitForXML( XmlDocument document )
        {
            Initialize( document, XmlTranslateType.XML);
        }

        /// <summary>
        /// 기본 생성자
        /// </summary>
        public XmlTranslator()
        {
            nodeHash.Clear();
        }
        /// <summary>
        /// 파일의 확장자를 이용해서 XSD/XML유형을 식별하여 분석/변환합니다.
        /// </summary>
        /// <param name="file">XML로 기술되어 저장된 파일(경로포함)</param>
        public XmlTranslator( string file )
        {
            XmlTranslateType type = XmlTranslateType.XML;
            string fileExt = System.IO.Path.GetExtension( file );
            if (fileExt.Equals( ".XSD", StringComparison.InvariantCultureIgnoreCase ))
                type = XmlTranslateType.XSD;

            XmlDocument document = new XmlDocument();
            document.Load( file );
            Initialize( document, type );
        }
        /// <summary>
        /// 주어진 XML문자열을 분석/변환합니다.
        /// </summary>
        /// <param name="xmlString"> XML문자열</param>
        /// <param name="type">분석/변환 처리 유형</param>
        public XmlTranslator( string xmlString, XmlTranslateType type )
        {
            XmlDocument document = new XmlDocument();
            document.LoadXml( xmlString );
            Initialize( document, type );
        }
        /// <summary>
        /// 주어진 XML문서객체를 분석/변환합니다.
        /// </summary>
        /// <param name="document">XML문서객체</param>
        /// <param name="type">분석/변환 처리 유형</param>
        public XmlTranslator( XmlDocument document, XmlTranslateType type )
        {
            Initialize( document, type );
        }

        private void Initialize( XmlDocument document, XmlTranslateType type)
        {
            nodeHash.Clear();
            this.nsmgr = NamespaceManager.GetNameSpace( document );
            navigator = document.CreateNavigator();

            XmlTag root = new XmlTag( "root", string.Empty );

            if (type == XmlTranslateType.XSD)
                InitializeXSD( navigator.SelectChildren( XPathNodeType.Element ), root );
            else
                InitializeXML( navigator.SelectChildren( XPathNodeType.Element ), root );

            this.translateType = type;
            nodeCollection.Clear();
            nodeCollection.AddRange( root.ChildNodes.ToArray() );
        }
        private void InitializeXSD( XPathNodeIterator iterator, XmlTag parent )
        {
            if (iterator == null) return;

            foreach (XPathNavigator navi in iterator)
            {
                if (navi.NodeType == XPathNodeType.Element)
                {
                    if (navi.LocalName.Equals( "element", StringComparison.InvariantCultureIgnoreCase ))
                    {
                        #region 엘리먼트를 정의하는 경우라면
                        XmlTag tag = null;
                        if (navi.HasAttributes)
                        {
                            #region 엘리먼트에 어트리뷰트 항목을 정의했다면 ... 
                            XPathNavigator attributeNavigator = navi.Clone();
                            if (attributeNavigator.MoveToFirstAttribute())
                            {
                                do
                                {
                                    if ((attributeNavigator.Name.Equals( "name", StringComparison.InvariantCultureIgnoreCase ))
                                        || (attributeNavigator.Name.Equals( "ref", StringComparison.InvariantCultureIgnoreCase ))
                                        )
                                    {
                                        // 모든 어트리뷰트를 처리하지 않는다.
                                        // 엘리먼트로서 의미가 부여되기 위해선, 'name' 혹은 'ref' 정도의 어트리뷰트가 요구된다.
                                        // 때문에 위 2가지 어트리뷰트가 발견되어야만, xmlTag로 인정을 하고, 생성한다.
                                        bool isref = false;
                                        if ((attributeNavigator.Name.Equals( "ref", StringComparison.InvariantCultureIgnoreCase )))
                                            isref = true;

                                        tag = new XmlTag( attributeNavigator.Value, string.Empty, isref, navi.TypedValue, parent.TagName.Equals( "root" ) ? null : parent );
                                        tag.Navigator = navi.Clone();

                                        if (tag.ParentNode == null) //사실상 루트노드일 경우
                                            tag.TagAttribute = NamespaceManager.GetNamespaceAttributeText( nsmgr );

                                        break;
                                    }
                                }
                                while (attributeNavigator.MoveToNextAttribute());
                            }
                            #endregion
                        }

                        if (tag != null)
                        {
                            // 위의 if문이 약간 이상하다고 생각될 수 있다.
                            // 실제로 엘리먼트이지만, 이름도, 속성도 없는 경우도 있기 때문에
                            // 최소한 이름 어트리뷰트가 있을때만 xmlTag가 생성이 되도록 했기 때문이다.

                            parent.ChildNodes.Add( tag );

                            if (!nodeHash.ContainsKey( tag.GetXPath() ))
                            {
                                List<IXmlTag> buff = new List<IXmlTag>();
                                buff.Add( tag );
                                nodeHash.Add( tag.GetXPath(), buff );
                            }
                            else
                            {
                                List<IXmlTag> buff = nodeHash[tag.GetXPath()] as List<IXmlTag>;
                                buff.Add( tag );
                            }
                        }

                        if (navi.HasChildren)
                        {
                            XPathNodeIterator childIterator = navi.SelectChildren( XPathNodeType.Element );
                            if (tag != null)
                                InitializeXSD( childIterator, tag );
                            else
                                InitializeXSD( childIterator, parent );
                        }
                        #endregion
                    }
                    else
                    {
                        if (navi.HasChildren)
                        {
                            XPathNodeIterator childIterator = navi.SelectChildren( XPathNodeType.Element );
                            InitializeXSD( childIterator, parent );
                        }
                    }
                } // end of 'if (navi.NodeType == XPathNodeType.Element)'
            } // end of 'foreach (XPathNavigator navi in iterator)'
        }
        private void InitializeXML( XPathNodeIterator iterator, XmlTag parent )
        {
            if (iterator == null) return;

            XmlTag tag = null;
            foreach (XPathNavigator navi in iterator)
            {
                tag = null;
                if (navi.NodeType == XPathNodeType.Element)
                {                    
                    tag = new XmlTag( navi.Name, navi.HasChildren ? "" : navi.Value, false, "string", parent.TagName.Equals( "root" ) ? null : parent );
                    parent.ChildNodes.Add( tag );
                    tag.Navigator = navi.Clone();

                    if (!nodeHash.ContainsKey( tag.GetXPath() ))
                    {
                        List<IXmlTag> buff = new List<IXmlTag>();
                        buff.Add( tag );
                        nodeHash.Add( tag.GetXPath(), buff );
                    }
                    else
                    {
                        List<IXmlTag> buff = nodeHash[tag.GetXPath()] as List<IXmlTag>;
                        buff.Add( tag );
                    }

                    if (tag.ParentNode == null) //사실상 루트노드일 경우
                        tag.TagAttribute = NamespaceManager.GetNamespaceAttributeText( nsmgr );
                    else
                    {
                        if (navi.HasAttributes)
                        {
                            XPathNavigator attributeNavigator = navi.Clone();
                            if (attributeNavigator.MoveToFirstAttribute())
                            {
                                StringBuilder attSb = new StringBuilder();
                                do
                                {
                                    attSb.AppendFormat( "{0}=\"{1}\" ", attributeNavigator.Name, attributeNavigator.Value );
                                } while (attributeNavigator.MoveToNextAttribute());

                                tag.TagAttribute = attSb.ToString();
                            }
                        }
                    }


                    if (navi.HasChildren)
                    {
                        XPathNodeIterator childIterator = navi.SelectChildren( XPathNodeType.Element );
                        if (childIterator.Count > 0)
                            InitializeXML( childIterator, tag );
                        else
                            tag.TagValue = navi.Value;
                    }
                }
            } // end of 'foreach (XPathNavigator navi in iterator)'
        }

        /// <summary>
        /// 주어진 구조화된 태그 컬렉션을 이용하여, 서식화된 xml문자열을 반환합니다.
        /// </summary>
        /// <param name="nodeCollection"></param>
        /// <returns></returns>
        public static string Parse( IList<IXmlTag> nodeCollection )
        {
            StringBuilder sb = new StringBuilder(1024);

            //TODO : 아래 문장은 XmlDocument를 이용해서 생성해야 한다.
            sb.AppendLine( "<?xml version=\"1.0\" encoding=\"utf-8\"?>" );

            foreach (XmlTag t in nodeCollection)
                sb.AppendFormat( "{0}\r\n", t.ToXmlString() );

            return sb.ToString();
        }

        /// <summary>
        /// 주어진 두 XML을 동기화합니다.
        /// </summary>
        /// <param name="xsdString">참조되는 xml</param>
        /// <param name="xmlString">모든 동기화된 내용이 반영되어 최종적으로 반환될 xml</param>
        /// <returns>xmlString값에 모든 동기화된 내용이 반영된 XML문서/변환 객체</returns>
        public static XmlTranslator SyncXml( string xsdString, string xmlString )
        {
            XmlTranslator xsd = new XmlTranslator( xsdString, XmlTranslateType.XSD );

            string xsdxml = XmlTranslator.Parse( xsd.NodeCollection );
            xsd = null;
            xsd = new XmlTranslator( xsdxml, XmlTranslateType.XML );

            XmlTranslator xml = new XmlTranslator( xmlString, XmlTranslateType.XML );
            return SyncXml( xsd, xml);
        }
        /// <summary>
        /// 주어진 두 XML을 동기화합니다.
        /// </summary>
        /// <param name="xmlFromXsd">참조되는 xml</param>
        /// <param name="xmlFromData">모든 동기화된 내용이 반영되어 최종적으로 반환될 xml</param>
        /// <returns>xmlString값에 모든 동기화된 내용이 반영된 XML문서/변환 객체</returns>
        public static XmlTranslator SyncXml( XmlTranslator xmlFromXsd, XmlTranslator xmlFromData )
        {
            #region 알고리즘
            // 병합하는 방법은 다음과 같다.
            // 1. xsd문서를 기준으로 루프를 돈다.
            // xsd의 xpath가 xml문서에 존재하면 skip !
            //                         존재하지 않으면 xml문서에 xpath 추가
            // 2. 위의 결과로 생성된 문서를 기준으로 루프를 돈다.
            // 문서의 xpath가 xsd문서에 존재하면 skip !
            //                          존재하지 않으면 문서에서 xpath를 삭제.
            // 3. 최종 반한된 xml문자열을 에디터에 올린다.
            #endregion

            IList<IXmlTag> xsdNodes = xmlFromXsd.nodeCollection;

            SyncSchema( xmlFromData.CurrentNamespaceManager, xsdNodes, xmlFromData.Navigator, false );

            xmlFromData.Rebuild();
            IList<IXmlTag> xmlNodes = xmlFromData.nodeCollection;

            SyncSchema( xmlFromData.CurrentNamespaceManager, xmlNodes, xmlFromXsd.Navigator, true );

            xmlNodes = xmlFromData.nodeCollection;
            string xmlXml = XmlTranslator.Parse( xmlNodes );

            XmlDocument doc = new XmlDocument();
            doc.LoadXml( xmlXml );
            XmlTranslator xt = new XmlTranslator();

            xt.InitForXML( doc );
            return xt;
        }
        private static void SyncSchema( XmlNamespaceManager xnsmgr, IList<IXmlTag> src, System.Xml.XPath.XPathNavigator target, bool isRemovedAction )
        {
            string xpath = string.Empty;
            Collections.IterIsolate iiso = new Collections.IterIsolate( src );
            foreach (IXmlTag tag in iiso)
            {

                xpath = tag.GetXPath();
                System.Xml.XPath.XPathNavigator current = target.SelectSingleNode( xpath, xnsmgr );
                if (current == null)
                {
                    if (isRemovedAction)
                    {
                        IXmlTag p = tag.ParentNode;
                        p.ChildNodeCollection.Remove( tag );
                    }
                    else
                    {
                        xpath = tag.ParentNode.GetXPath();
                        System.Xml.XPath.XPathNavigator parent = target.SelectSingleNode( xpath, xnsmgr );
                        parent.AppendChild( tag.Navigator.Clone() );
                    }
                }
                else
                {
                    if (tag.ChildNodesCount > 0)
                    {
                        SyncSchema( xnsmgr, tag.ChildNodeCollection, target, isRemovedAction );
                    }
                }
            }
        }

        #region IDisposable 멤버
        private bool isDisposed = false;
        /// <summary>
        /// 모든 자원을 해제합니다.
        /// </summary>
        public void Dispose()
        {
            this.Dispose( true );
            GC.SuppressFinalize( this );
        }
        /// <summary>
        /// 모든 자원을 해제합니다.
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose( bool disposing )
        {
            if (isDisposed) return;
            if (disposing)
            {
                // 순서가 중요합니다.

                if (nodeHash != null)
                {
                    nodeHash.Clear();
                    nodeHash = null;
                }

                if (nodeCollection != null)
                {
                    foreach (IXmlTag child in nodeCollection)
                        child.Dispose();

                    nodeCollection.Clear();
                    nodeCollection = null;
                }

                if (navigator != null)
                    navigator = null;

                if (nsmgr != null)
                    nsmgr = null;
            }
            isDisposed = true;
        }

        #endregion
    }
}
